package com.zkh.myutils.scheduled.cron;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.function.Predicate;

import com.zkh.myutils.utils.ArrayUtils;
import com.zkh.myutils.utils.StringUtils;

/**
 * Cron表达式解析器
 * @author zkh
 */
class CronParser {
	
	//每部分名称
	public static String[] partNames = {"秒", "分", "小时", "日期或周", "月份", "年"};
	
	/**
	 * 解析表达式，表达式顺序为：秒 分 小时 日期 月份 星期 年(可选)<br>
	 * 格式如下：<br>
	 * 秒、分、时、月、年 -> , - * / <br>
	 * 日 -> , - * ? / L W C <br>
	 * 星期 -> , - * ? / L C #
	 * @param cb CronBean
	 * @return
	 */
	public static int parse(CronBean cb) {
		//当前日期
		Calendar calendar = Calendar.getInstance();
		//表达式各部分：秒 分 小时 日期 月份 星期 年(可选)
		String[] parts = cb.getCronParts();
		//当前解析步骤（0表示解析秒，6表示解析年）
		int step = 0;
		//捕获异常
		try {
			//秒
			parseTypeCommon(parts[0], 0, 59, cb.getSeconds());step++;
			//分
			parseTypeCommon(parts[1], 0, 59, cb.getMinutes());step++;
			//小时
			parseTypeCommon(parts[2], 0, 23, cb.getHours());step++;
			//日期和周
			parseTypeDateWeek(cb);step++;
			//月份
			parseTypeCommon(parts[4], 1, 12, cb.getMonths());step++;
			//年
			if(StringUtils.isEmpty(parts[6])) {
				//遍历添加年份
				for(int i=calendar.get(Calendar.YEAR); i<=2099; i++) {
					cb.getYears().add(i);
				}
			}else {
				parseTypeCommon(parts[6], calendar.get(Calendar.YEAR), 2099, cb.getYears());
			}
			//返回当前月份
			return calendar.get(Calendar.MONTH) + 1;
		}catch(CronParseException e) {
			throw new CronParseException(partNames[step] + "部分" + e.getMessage(), e);
		}
	}
	
	/**
	 * 解析公共类型（即含 , - * / 符号）
	 * @param part 待解析部分
	 * @param minValue 最小值
	 * @param maxValue 最大值
	 * @param container 解析后容器
	 */
	private static void parseTypeCommon(String part, int minValue, int maxValue, List<Integer> container) {
		parseTypeCommon(part, minValue, maxValue, container, true);
	}
	
	/**
	 * 解析公共类型（即含 , - * / 符号）
	 * @param part 待解析部分
	 * @param minValue 最小值
	 * @param maxValue 最大值
	 * @param container 解析后容器
	 * @param throwIfNotMatched 是否在未匹配上时，抛出异常
	 */
	private static boolean parseTypeCommon(String part, int minValue, int maxValue, List<Integer> container, boolean throwIfNotMatched) {
		//星号
		boolean matched = fillStar(part, minValue, maxValue, container);
		//逗号
		matched = matched || fillCommas(part, minValue, maxValue, container);
		//连接号
		matched = matched || fillSubtracts(part, minValue, maxValue, container);
		//斜线
		matched = matched || fillBias(part, minValue, maxValue, container);
		//未匹配上，抛出异常
		if(!matched && throwIfNotMatched) {
			throw new CronParseException("格式错误");
		}
		//返回匹配结果
		return matched;
	}
	
	/**
	 * 匹配星号
	 * @param part 待解析部分
	 * @param minValue 最小值
	 * @param maxValue 最大值
	 * @param container 解析后容器
	 * @return 返回是否匹配上
	 */
	private static boolean fillStar(String part, int minValue, int maxValue, List<Integer> container) {
		//是否匹配
		boolean matched = "*".equals(part);
		//匹配
		if(matched) {
			//如果是星期，转为按日期取每天
			maxValue = maxValue==7 ? 31 : maxValue;
			//遍历取值
			for(int i=minValue; i<=maxValue; i++) container.add(i);
		}
		//返回是否匹配
		return matched;
	}
	
	/**
	 * 匹配逗号
	 * @param part 待解析部分
	 * @param minValue 最小值
	 * @param maxValue 最大值
	 * @param container 解析后容器
	 * @return 返回是否匹配上
	 */
	private static boolean fillCommas(String part, int minValue, int maxValue, List<Integer> container) {
		//是否匹配
		boolean matched = part.matches("\\d+(,\\d+)*");
		// 匹配
		if(matched) {
			//分割
			Integer[] ps = ArrayUtils.convertType(part.split(","), Integer.class, Integer::valueOf);
			//非星期
			if(maxValue!=7) {
				//遍历
				for(int t: ps) {
					//检查范围
					if(t<minValue || t>maxValue) {
						throw new CronParseException("超出范围");
					}
					//保存
					container.add(t);
				}
			}//星期
			else {
				addWeekToDate(container, week->ArrayUtils.indexOf(week, ps)>-1);
			}
		}
		//返回是否匹配
		return matched;
	}

	/**
	 * 匹配减号（-）
	 * @param part 待解析部分
	 * @param minValue 最小值
	 * @param maxValue 最大值
	 * @param container 解析后容器
	 * @return 返回是否匹配上
	 */
	private static boolean fillSubtracts(String part, int minValue, int maxValue, List<Integer> container) {
		//是否匹配
		boolean matched = part.matches("\\d+\\-\\d+(/\\d+)?");
		// 匹配
		if(matched) {
			//以间隔结尾
			int inter = part.indexOf("/");
			//间隔
			int interval = inter==-1 ? 1 : Integer.parseInt(part.substring(inter+1));
			//分割
			String[] ps = (inter==-1 ? part : part.substring(0, inter)).split("\\-");
			//取范围
			int min = Integer.parseInt(ps[0]), max = Integer.parseInt(ps[1]);
			//超出范围
			if(min<minValue || max>maxValue) {
				throw new CronParseException("超出范围");
			}
			if(min>max) {
				throw new CronParseException("最小值不能比最大值大");
			}
			//有效值
			List<Integer> values = new ArrayList<>();
			//遍历
			for(int i=min; i<=max; i+=interval) {
				values.add(i);
			}
			//非星期
			if(maxValue!=7) {
				container.addAll(values);
			}//星期
			else {
				addWeekToDate(container, values::contains);
			}
		}
		//返回是否匹配
		return matched;
	}
	
	/**
	 * 匹配斜线（/）
	 * @param part 待解析部分
	 * @param minValue 最小值
	 * @param maxValue 最大值
	 * @param container 解析后容器
	 * @return 返回是否匹配上
	 */
	private static boolean fillBias(String part, int minValue, int maxValue, List<Integer> container) {
		//是否匹配
		boolean matched = part.matches("\\d+/\\d+");
		// 匹配
		if(matched) {
			//分割
			String[] ps = part.split("/");
			//取开始和间隔
			int start = Integer.parseInt(ps[0]), internal = Integer.parseInt(ps[1]);
			//超出范围
			if(start<minValue) {
				throw new CronParseException("超出范围");
			}
			//候选值
			List<Integer> values = new ArrayList<>();
			//遍历
			while(start<=maxValue) {
				//保存
				values.add(start);
				//累加
				start += internal;
			}
			//非星期
			if(maxValue!=7) {
				container.addAll(values);
			}//星期
			else {
				addWeekToDate(container, values::contains);
			}
		}
		//返回是否匹配
		return matched;
	}
	
	/**
	 * 将星期条件加到日期字段中
	 * @param container 日期容器
	 * @param predicate 筛选条件
	 */
	private static void addWeekToDate(List<Integer> container, Predicate<Integer> predicate) {
		//初始化日期，并设置为1号
		Calendar calendar = Calendar.getInstance();calendar.set(Calendar.DATE, 1);
		//记录月份
		int month = calendar.get(Calendar.MONTH);
		//遍历
		while(calendar.get(Calendar.MONTH)==month) {
			//获取星期
			int week = calendar.get(Calendar.DAY_OF_WEEK);
			//在指定范围内
			if(predicate.test(week)) {
				//保存
				container.add(calendar.get(Calendar.DATE));
			}
			calendar.add(Calendar.DATE, 1);
		}
	}

	/**
	 * 解析日期或星期<br>
	 * 日期类型（即含 , - * / ? L W C）<br>
	 * 周类型（即含 , - * / ? L C #）<br>
	 */
	private static void parseTypeDateWeek(CronBean cb) {
		//获取所需数据
		String datestr = cb.getCronParts()[3], weekstr = cb.getCronParts()[5];
		List<Integer> dateList = cb.getDates();
		//检查问号
		boolean dateQM = "?".equals(datestr), weekQM = "?".equals(weekstr);
		//日期和星期必须有一个为问号
		if(dateQM && weekQM || !dateQM && !weekQM) {
			throw new CronParseException("日期部分和星期部分有且只能有一个为问号");
		}
		//解析公共部分
		boolean matched = weekQM ? parseTypeCommon(datestr, 1, 31, dateList, false) : parseTypeCommon(weekstr, 1, 7, dateList, false);
		//未匹配到，做特殊字符解析
		if(!matched) {
			//日期L
			if(datestr.matches("(\\d{1,2})?L")) {
				parseDateL(cb, datestr);
			}//日期W
			else if(datestr.matches("([1-9]|[12]\\d|3[01])W")) {
				parseDateW(cb, datestr);
			}//日期C
			else if(datestr.matches("([1-9]|[12]\\d|30)C")) {
				throw new CronParseException("系统暂不支持含C的表达式");
			}//日期LW
			else if("LW".equals(datestr) || "WL".equals(datestr)) {
				parseDateLW(cb, datestr);
			}//星期L
			else if(weekstr.matches("[1-7]?L")) {
				parseWeekL(cb, weekstr);
			}//星期C
			else if(weekstr.matches("[1-6]C")) {
				throw new CronParseException("系统暂不支持含C的表达式");
			}//星期#
			else if(weekstr.matches("[1-7]#[1-5]")) {
				parseWeekSharp(cb, weekstr);
			}else {
				throw new CronParseException("日期或星期部分格式错误");
			}
		}
	}
	
	/**
	 * 解析Date部分中的L
	 */
	private static void parseDateL(CronBean cb, String partStr) {
		cb.setReCalculator(cronBean->{
			//当前日期
			Calendar calendar = Calendar.getInstance();
			//获取L前的数字
			String n = "L".equals(partStr) ? "1" : partStr.substring(0, partStr.length()-1);
			//记录月份，防止计算跨月
			int month = calendar.get(Calendar.MONTH);
			//处理日期（日期设置为1号，月份加1，日期减跨度）
			calendar.set(Calendar.DATE, 1);
			calendar.add(Calendar.MONTH, 1);
			calendar.add(Calendar.DATE, -1 * Integer.parseInt(n));
			//检查是否跨月，未跨月，则保存
			if(month==calendar.get(Calendar.MONTH)) {
				cronBean.setDates(ArrayUtils.asList(calendar.get(Calendar.DATE)));
			}
		});
	}
	
	/**
	 * 解析Date部分中的W
	 */
	private static void parseDateW(CronBean cb, String partStr) {
		cb.setReCalculator(cronBean->{
			//当前日期
			Calendar calendar = Calendar.getInstance();
			//获取W前的数字
			String n = partStr.substring(0, partStr.length()-1);
			//记录月份，防止计算跨月
			int month = calendar.get(Calendar.MONTH);
			//设置日期
			calendar.set(Calendar.DATE, Integer.parseInt(n));
			//检查是否跨月，未跨月，则保存
			if(month==calendar.get(Calendar.MONTH)) {
				//当前星期（1是星期日，7是星期六）
				int week = calendar.get(Calendar.DAY_OF_WEEK);
				//非工作日，偏移日期
				if(week==1){
					calendar.add(Calendar.DATE, 1);
					//跨月
					if(month==calendar.get(Calendar.MONTH)) {
						calendar.add(Calendar.DATE, -3);
					}
				}else if(week==7) {
					calendar.add(Calendar.DATE, -1);
					//跨月
					if(month==calendar.get(Calendar.MONTH)) {
						calendar.add(Calendar.DATE, 3);
					}
				}
				cronBean.setDates(ArrayUtils.asList(calendar.get(Calendar.DATE)));
			}
		});
	}
	
	/**
	 * 解析Date部分中的LW
	 */
	private static void parseDateLW(CronBean cb, String partStr) {
		cb.setReCalculator(cronBean->{
			//当前日期
			Calendar calendar = Calendar.getInstance();
			//处理日期（日期设置为1号，月份加1，日期-1）
			calendar.set(Calendar.DATE, 1);
			calendar.add(Calendar.MONTH, 1);
			calendar.add(Calendar.DATE, -1);
			//当前星期（1是星期日，7是星期六）
			int week = calendar.get(Calendar.DAY_OF_WEEK);
			//日期偏移
			if(week==1 || week==7) {
				calendar.add(Calendar.DATE, week==1 ? -2 : -1);
			}
			cronBean.setDates(ArrayUtils.asList(calendar.get(Calendar.DATE)));
		});
	}
	
	/**
	 * 解析Week部分中的L
	 */
	private static void parseWeekL(CronBean cb, String partStr) {
		cb.setReCalculator(cronBean->{
			//当前日期
			Calendar calendar = Calendar.getInstance();
			//设置日期为1号
			calendar.set(Calendar.DATE, 1);
			//长度
			int len = partStr.length();
			//记录月份，计算是否跨月
			int month = calendar.get(Calendar.MONTH);
			//获取前缀
			int prefix = len==2 ? Integer.parseInt(partStr.substring(0, 1)) : 7;
			//设置星期
			calendar.set(Calendar.DAY_OF_WEEK, prefix);
			//如果向前跨月，则偏移日期
			if(calendar.get(Calendar.MONTH)!=month) {
				calendar.add(Calendar.DATE, 7);	//往后移7天
			}
			//日期集合
			List<Integer> dateList = new ArrayList<>();
			//遍历
			while(calendar.get(Calendar.MONTH)==month) {
				dateList.add(calendar.get(Calendar.DATE));
				calendar.add(Calendar.DATE, 7);	//往后移7天，取下周
			}
			//保存日期
			cronBean.setDates(len==1 ? dateList : ArrayUtils.asList(dateList.get(dateList.size()-1)));
		});
	}
	
	/**
	 * 解析Week部分中的#
	 */
	private static void parseWeekSharp(CronBean cb, String partStr) {
		cb.setReCalculator(cronBean->{
			//当前日期
			Calendar calendar = Calendar.getInstance();
			//设置日期为1号
			calendar.set(Calendar.DATE, 1);
			//记录月份，计算是否跨月
			int month = calendar.get(Calendar.MONTH);
			//取值
			int week = Integer.parseInt(partStr.substring(0, 1)),
					times = Integer.parseInt(partStr.substring(2, 3));
			//设置星期
			calendar.set(Calendar.DAY_OF_WEEK, week);
			//如果向前跨月，则偏移日期
			if(calendar.get(Calendar.MONTH)!=month) {
				calendar.add(Calendar.DATE, 7);	//往后移7天
			}
			//日期集合
			List<Integer> dateList = new ArrayList<>();
			//遍历
			while(calendar.get(Calendar.MONTH)==month) {
				dateList.add(calendar.get(Calendar.DATE));
				calendar.add(Calendar.DATE, 7);	//往后移7天，取下周
			}
			//保存日期
			if(times<=dateList.size()) {
				cronBean.setDates(ArrayUtils.asList(dateList.get(times-1)));
			}
		});
	}
}
